package afcp.alumni.dao.base;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import afcp.alumni.model.base.SAPIEntity;
import afcp.alumni.model.db.Member;

import com.google.appengine.api.search.DeleteException;
import com.google.appengine.api.search.Document;
import com.google.appengine.api.search.GetIndexesRequest;
import com.google.appengine.api.search.GetRequest;
import com.google.appengine.api.search.GetResponse;
import com.google.appengine.api.search.Index;
import com.google.appengine.api.search.IndexSpec;
import com.google.appengine.api.search.PutException;
import com.google.appengine.api.search.Results;
import com.google.appengine.api.search.ScoredDocument;
import com.google.appengine.api.search.SearchException;
import com.google.appengine.api.search.SearchService;
import com.google.appengine.api.search.SearchServiceFactory;
import com.google.appengine.api.search.StatusCode;

/**
 * 
 * @author Antoine
 * 
 */
public class DaoSearchAPI {
	
	private static final Log logger = LogFactory.getLog(DaoSearchAPI.class);

	private static DaoSearchAPI instance = null;

	public static DaoSearchAPI getInstance() {
		if (instance == null) {
			instance = new DaoSearchAPI();
		}
		return instance;
	}
	
	private DaoSearchAPI() {
	}
	
//	private Dao<T> daoDatastore = null;
//
//	public DaoSearchAPI(Dao<T> daoDatastore) {
//		this.daoDatastore = daoDatastore;
//	}

	protected Index getIndex(SAPIEntity entity) {
		return getIndex(entity.getClass().getSimpleName());
	}
	
	protected Index getIndex(String className) {
		IndexSpec indexSpec = IndexSpec.newBuilder().setName(className + "Index").build();
		return SearchServiceFactory.getSearchService().getIndex(indexSpec);
	}

	public boolean add(SAPIEntity entity) {
		
		try {
			Index index = getIndex(entity);
			Document document = entity.getDocument();
			try {
				index.put(document);
				return true;
			} catch (PutException e) {
			    if (StatusCode.TRANSIENT_ERROR.equals(e.getOperationResult().getCode())) {
					index.put(document);
					return true;
			    }
			}
		} catch (Exception e) {
			logger.error("Could not put in index the entity with id: " + entity.getId(), e);
		}

		return false;
	}
	
	/**
	 * Contrainte d'utilisation : toutes les entités doivent être de la même classe ! La classe
	 * de la première entité sera utilisée pour déterminer l'index dans lequel les entités seront stockées.
	 * @param entities
	 * @return
	 */
	public <U extends SAPIEntity> boolean add(List<U> entities) {
		List<Document> documents = new LinkedList<>();
		try {
			Index index = null;
			for (SAPIEntity entity : entities) {
				if (index == null) {
					index = getIndex(entity);
				}
				Document document = entity.getDocument();
				documents.add(document);
			}
			try {
				logger.info("Add documents to the index");
				index.put(documents);
				return true;
			} catch (PutException e) {
			    if (StatusCode.TRANSIENT_ERROR.equals(e.getOperationResult().getCode())) {
					logger.info("Failed indexing; retrying...");
					index.put(documents);
					return true;
			    }
			}
		} catch (Exception e) {
			logger.error("Could not put in index the entities.", e);
		}

		return false;
	}
	
	public boolean remove(String id, String className) {
		try {
			Index index = getIndex(className);
			try {
				index.delete(id);
				return true;
			} catch (DeleteException e) {
			    if (StatusCode.TRANSIENT_ERROR.equals(e.getOperationResult().getCode())) {
					index.delete(id);
					return true;
			    }
			}
		} catch (Exception e) {
			logger.error("Could not delete from index the entity with id: " + id, e);
		}
		return false;
	}
	
	/**
	 * Contrainte d'utilisation : toutes les entités doivent être de la même classe ! La classe
	 * de la première entité sera utilisée pour déterminer l'index dans lequel les entités seront supprimées.
	 * @param entities
	 * @return
	 */
	public /*<U extends SAPIEntity>*/ boolean remove(List<String> ids, String className) {
		List<String> documentIds = new LinkedList<>();
		try {
			if (ids.isEmpty()) {
				return true;
			}
			Index index = getIndex(className);
			for (String id : ids) {
//				if (index == null) {
//					index = getIndex(entity);
//				}
				documentIds.add(id);
			}
			try {
				index.delete(documentIds);
				return true;
			} catch (DeleteException e) {
			    if (StatusCode.TRANSIENT_ERROR.equals(e.getOperationResult().getCode())) {
					index.delete(documentIds);
					return true;
			    }
			}
		} catch (Exception e) {
			logger.error("Could not delete from index the entities.", e);
		}

		return false;
	}
	
	public boolean clearIndex() {

		// Get the SearchService for the default namespace
		SearchService searchService = SearchServiceFactory.getSearchService();

		// Get the first page of indexes available and retrieve schemas
		GetResponse<Index> response = searchService
				.getIndexes(GetIndexesRequest.newBuilder()
						.setSchemaFetched(true)
						.build());
		// TODO essayer de supprimer setSchemaFetched(true), ou de le passer à
		// false

		boolean success = true;
		for (Index index : response) {
			try {

				GetRequest request = GetRequest.newBuilder()
				// .setLimit(500)
				// .setStartId("some-id")
						.setReturningIdsOnly(true).build();

				List<String> documentIds = new LinkedList<>();

				for (Document document : index.getRange(request)) {
					documentIds.add(document.getId());
				}

				logger.info("Deleting documents of index " + index.getName());
				index.delete(documentIds);
			} catch (RuntimeException e) {
				logger.error("Failed to delete documents", e);
				success = false;
			}

		}

		return success;
	}
	
	/**
	 * 
	 * @param queryString
	 *            the query used for this search. Example: <code>"robert"</code>
	 *            .
	 * @param className
	 *            the class name in which the search is processed. Example:
	 *            <p>
	 *            <code>entity.getClassObject().getSimpleName()</code>
	 *            </p>
	 *            Currently, only {@link Member} entities are indexed.
	 * @return a list of Ids of the documents found with this query. These ids
	 *         may be used to find the entities on the datastore.
	 */
	public List<String> search(String queryString, String className) {
		// TODO ptet améliorable si un type générique est donné à la classe et qu'un DAO spécifique donne le type.
		// éventuellement passer en protected et les enfants surchargent et utilisent une méthode getClassObject()
//		String className = clazz.getSimpleName();
		try {
			Index index = getIndex(className);
			Results<ScoredDocument> results = null;
			try {
			    // Query the index.
				logger.info("Run search in SAPI with query String: " + queryString);
			    results = index.search(queryString);

			} catch (SearchException e) {
			    if (StatusCode.TRANSIENT_ERROR.equals(e.getOperationResult().getCode())) {
				    results = index.search(queryString);
			    } else {
			    	throw e;
			    }
			}
		    List<String> ids = new ArrayList<>();
		    for (ScoredDocument document : results) {
		    	ids.add(document.getId());
		    }
//			return this.daoDatastore.find(ids);
			return ids;
		} catch (Exception e) {
			logger.error("Could not search in index the entity:" + className + " with query: " + queryString, e);
		}
		return null;
	}
}
